﻿
using System;
using System.Globalization;

namespace EmperialApps.WeatherSpark.Data {

    /// <summary>Represents a geographic location on the globe.</summary>
    public partial struct Coordinate : IComparable<Coordinate> {

        /// <summary>
        /// Initializes a new instance of the <see cref="Coordinate"/> struct with the specified latitude and longitude.
        /// </summary>
        /// <param name="latitude">The geographic latitude of the location.</param>
        /// <param name="longitude">The geographic longitude of the location.</param>
        public Coordinate( double latitude, double longitude )
            : this( GetState( latitude, longitude ) ) { }

        /// <summary>The geographic latitude of the location.</summary>
        public double Latitude {
            get {
                ushort stateLatitude = unchecked( (ushort)(this.State >> LatitudeShift) );
                double latitude = Deserialize( stateLatitude );
                return latitude;
            }
        }

        /// <summary>The geographic longitude of the location.</summary>
        public double Longitude {
            get {
                ushort stateLongitude = unchecked( (ushort)this.State );
                double longitude = Deserialize( stateLongitude );
                return longitude;
            }
        }

        /// <inheritdoc/>
        public int CompareTo( Coordinate other ) {
            return this.State.CompareTo( other.State );
        }

        /// <inheritdoc/>
        public override string ToString( ) {
            return this.ToString( display: true );
        }

        /// <summary>Returns a string representation of the coordinate.</summary>
        public string ToString( bool display ) {
            string separator;
            double latitude = this.Latitude;
            double longitude = this.Longitude;
            if( display ) {
                separator = LocalDisplaySeparator;
            }
            else {
                separator = FileNameSeparator;
                latitude = Math.Round( latitude * PrecisionFactor );
                longitude = Math.Round( longitude * PrecisionFactor );
            }

            return latitude + separator + longitude;
        }

        /// <summary>Converts a string representation into a coordinate.</summary>
        public static Coordinate Parse( string input ) {
            double factor = 1;
            IFormatProvider formatProvider = null;
            int separatorIndex = input.IndexOf( LocalDisplaySeparator );

            if( separatorIndex < 0 ) {
                separatorIndex = input.IndexOf( InvariantDisplaySeparator );
                formatProvider = CultureInfo.InvariantCulture;
            }

            if( separatorIndex < 0 ) {
                separatorIndex = input.IndexOf( FileNameSeparator );
                factor = PrecisionFactor;
            }

            string inputLatitude = input.Substring( 0, separatorIndex );
            string inputLongitude = input.Substring( separatorIndex + 1 );
            double latitude = double.Parse( inputLatitude, formatProvider ) / factor;
            double longitude = double.Parse( inputLongitude, formatProvider ) / factor;
            return new Coordinate( latitude, longitude );
        }

        /// <summary>Converts the string representation of a latitude and a longitude into a coordinate.</summary>
        public static Coordinate Parse( string latitudeInput, string longitudeInput, bool invertLongitude ) {
            double longitudeMultiple = invertLongitude ? -1 : 1;
            double latitude, longitude;
            try {
                latitude = double.Parse( latitudeInput );
                longitude = double.Parse( longitudeInput ) * longitudeMultiple;
            }
            catch( FormatException ex ) {
                string message = string.Format( "Could not parse coordinate from '{0}' and '{1}' values.", latitudeInput, longitudeInput );
                throw new FormatException( message, ex );
            }

            return new Coordinate( latitude, longitude );
        }

        #region Private Members

        private const double PrecisionFactor = 100;
        private const int LatitudeShift = 8 * sizeof( ushort );
        private const string InvariantDisplaySeparator = ",";
        private const string FileNameSeparator = "_";

        private static string LocalDisplaySeparator {
            get { return CultureInfo.CurrentCulture.TextInfo.ListSeparator; }
        }

        private static uint GetState( double latitude, double longitude ) {
            ushort stateLatitude = Serialize( latitude );
            ushort stateLongitude = Serialize( longitude );
            int state = (stateLatitude << LatitudeShift) | stateLongitude;
            return unchecked( (uint)state );
        }

        private static ushort Serialize( double value ) {
            short rounded = checked( (short)Math.Round( value * PrecisionFactor ) );
            return unchecked( (ushort)rounded );
        }

        private static double Deserialize( ushort value ) {
            double rounded = unchecked( (short)value );
            return rounded / PrecisionFactor;
        }

        #endregion
    }

}
